/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification of Java bytecode.
 *
 * Copyright (c) 2002-2018 GuardSquare NV
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free
 * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard.backport;

import proguard.classfile.*;
import proguard.classfile.attribute.*;
import proguard.classfile.attribute.visitor.*;
import proguard.classfile.constant.*;
import proguard.classfile.editor.*;
import proguard.classfile.instruction.*;
import proguard.classfile.instruction.visitor.*;
import proguard.classfile.util.*;
import proguard.classfile.visitor.*;
import proguard.util.*;

import java.util.*;

/**
 * This ClassVisitor converts all lambda expressions in the visited classes to anonymous inner classes.
 *
 * @author Thomas Neidhart
 */
public class LambdaExpressionConverter extends SimplifiedVisitor implements ClassVisitor,

    // Implementation interfaces.
    MemberVisitor, AttributeVisitor, InstructionVisitor {
    private static final String LAMBDA_SINGLETON_FIELD_NAME = "INSTANCE";

    private final ClassPool programClassPool;
    private final ClassPool libraryClassPool;
    private final MultiValueMap<String, String> injectedClassNameMap;
    private final ClassVisitor extraClassVisitor;

    private final Map<Integer, LambdaExpression> lambdaExpressionMap;
    private final CodeAttributeEditor codeAttributeEditor;
    private final MemberRemover memberRemover;

    public LambdaExpressionConverter(ClassPool programClassPool, ClassPool libraryClassPool,
        MultiValueMap<String, String> injectedClassNameMap, ClassVisitor extraClassVisitor) {
        this.programClassPool = programClassPool;
        this.libraryClassPool = libraryClassPool;
        this.injectedClassNameMap = injectedClassNameMap;
        this.extraClassVisitor = extraClassVisitor;

        this.lambdaExpressionMap = new HashMap<Integer, LambdaExpression>();
        this.codeAttributeEditor = new CodeAttributeEditor(true, true);
        this.memberRemover = new MemberRemover();
    }

    // Implementations for ClassVisitor.

    @Override
    public void visitLibraryClass(LibraryClass libraryClass) {}

    @Override
    public void visitProgramClass(ProgramClass programClass) {
        lambdaExpressionMap.clear();
        programClass.accept(new LambdaExpressionCollector(lambdaExpressionMap));

        for (LambdaExpression lambdaExpression : lambdaExpressionMap.values()) {
            ProgramClass lambdaClass = createLambdaClass(lambdaExpression);

            // Add the converted lambda class to the program class pool
            // and the injected class name map.
            programClassPool.addClass(lambdaClass);
            injectedClassNameMap.put(programClass.getName(), lambdaClass.getName());

            if (extraClassVisitor != null) {
                extraClassVisitor.visitProgramClass(lambdaClass);
            }
        }

        if (!lambdaExpressionMap.isEmpty()) {
            // Replace all InvokeDynamic instructions.
            programClass.accept(new AllMethodVisitor(new AllAttributeVisitor(this)));

            // Initialize the hierarchy and references of all lambda classes.
            for (LambdaExpression lambdaExpression : lambdaExpressionMap.values()) {
                lambdaExpression.lambdaClass.accept(
                    new MultiClassVisitor(new ClassSuperHierarchyInitializer(programClassPool, libraryClassPool),
                        new ClassSubHierarchyInitializer(),
                        new ClassReferenceInitializer(programClassPool, libraryClassPool)));
            }

            // Remove deserialization hooks as they are no longer needed.
            programClass.methodsAccept(this);
            memberRemover.visitProgramClass(programClass);
        }
    }

    // Implementations for AttributeVisitor.

    @Override
    public void visitAnyAttribute(Clazz clazz, Attribute attribute) {}

    @Override
    public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute) {
        codeAttributeEditor.reset(codeAttribute.u4codeLength);

        codeAttribute.instructionsAccept(clazz, method, this);

        if (codeAttributeEditor.isModified()) {
            codeAttributeEditor.visitCodeAttribute(clazz, method, codeAttribute);
        }
    }

    // Implementations for InstructionVisitor.

    @Override
    public void visitAnyInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset,
        Instruction instruction) {}

    @Override
    public void visitConstantInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset,
        ConstantInstruction constantInstruction) {
        if (constantInstruction.opcode == InstructionConstants.OP_INVOKEDYNAMIC) {
            ProgramClass programClass = (ProgramClass)clazz;

            InvokeDynamicConstant invokeDynamicConstant =
                (InvokeDynamicConstant)programClass.getConstant(constantInstruction.constantIndex);

            int bootstrapMethodIndex = invokeDynamicConstant.getBootstrapMethodAttributeIndex();
            if (lambdaExpressionMap.containsKey(bootstrapMethodIndex)) {
                LambdaExpression lambdaExpression = lambdaExpressionMap.get(bootstrapMethodIndex);
                String lambdaClassName = lambdaExpression.getLambdaClassName();

                InstructionSequenceBuilder builder = new InstructionSequenceBuilder(programClass);

                if (lambdaExpression.isStateless()) {
                    builder.getstatic(lambdaClassName, LAMBDA_SINGLETON_FIELD_NAME,
                        ClassUtil.internalTypeFromClassName(lambdaClassName));
                } else {
                    int maxLocals = codeAttribute.u2maxLocals;

                    String methodDescriptor = lambdaExpression.getConstructorDescriptor();
                    int parameterSize = ClassUtil.internalMethodParameterSize(methodDescriptor);

                    // TODO: the special optimization in case there is only 1
                    // parameter has been disabled as the used stack
                    // manipulation instructions might confuse the class
                    // converter (testcase 1988).
                    if (parameterSize == 1 && false) {
                        // If only 1 parameter is captured by the lambda expression,
                        // and it is a Category 1 value, we can avoid storing the
                        // current stack to variables.
                        builder.new_(lambdaClassName).dup_x1().swap().invokespecial(lambdaClassName,
                            ClassConstants.METHOD_NAME_INIT, methodDescriptor);
                    } else {
                        // More than 1 (or a Category 2) parameter is captured
                        // by the lambda expression. We need to store the current
                        // call stack to variables, create the lambda instance and
                        // load the call stack again from the temporary variables.

                        // Collect the argument types.
                        InternalTypeEnumeration typeEnumeration = new InternalTypeEnumeration(methodDescriptor);
                        List<String> types = new ArrayList<String>();
                        while (typeEnumeration.hasMoreTypes()) {
                            types.add(typeEnumeration.nextType());
                        }

                        // Store the current call stack in reverse order
                        // into temporary variables.
                        int variableIndex = maxLocals;
                        ListIterator<String> typeIterator = types.listIterator(types.size());
                        while (typeIterator.hasPrevious()) {
                            String type = typeIterator.previous();

                            builder.store(variableIndex, type);
                            variableIndex += ClassUtil.internalTypeSize(type);
                        }

                        // Create the lambda instance.
                        builder.new_(lambdaClassName);
                        builder.dup();

                        // Reconstruct the call stack by loading it from
                        // the temporary variables.
                        typeIterator = types.listIterator();
                        while (typeIterator.hasNext()) {
                            String type = typeIterator.next();

                            int variableSize = ClassUtil.internalTypeSize(type);
                            variableIndex -= variableSize;
                            builder.load(variableIndex, type);
                        }

                        builder.invokespecial(lambdaClassName, ClassConstants.METHOD_NAME_INIT, methodDescriptor);
                    }
                }

                codeAttributeEditor.replaceInstruction(offset, builder.instructions());
            }
        }
    }

    // Implementations for MemberVisitor.

    public void visitAnyMember(Clazz clazz, Member member) {}

    public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod) {
        if (isDeserializationHook(programClass, programMethod)) {
            memberRemover.visitProgramMethod(programClass, programMethod);
        }
    }

    // Small utility methods.

    private static final String METHOD_NAME_DESERIALIZE_LAMBDA = "$deserializeLambda$";
    private static final String METHOD_TYPE_DESERIALIZE_LAMBDA =
        "(Ljava/lang/invoke/SerializedLambda;)Ljava/lang/Object;";

    private static boolean isDeserializationHook(Clazz clazz, Method method) {
        return method.getName(clazz).equals(METHOD_NAME_DESERIALIZE_LAMBDA)
            && method.getDescriptor(clazz).equals(METHOD_TYPE_DESERIALIZE_LAMBDA)
            && hasFlag(method, ClassConstants.ACC_PRIVATE | ClassConstants.ACC_STATIC | ClassConstants.ACC_SYNTHETIC);
    }

    private static boolean hasFlag(Member member, int flag) {
        return (member.getAccessFlags() & flag) == flag;
    }

    private ProgramClass createLambdaClass(LambdaExpression lambdaExpression) {
        String lambdaClassName = lambdaExpression.getLambdaClassName();

        ProgramClass lambdaClass = new ProgramClass(ClassConstants.CLASS_VERSION_1_5, 1, new Constant[10], 0, 0, 0);

        ConstantPoolEditor constantPoolEditor = new ConstantPoolEditor(lambdaClass);

        lambdaClass.u2thisClass = constantPoolEditor.addClassConstant(lambdaClassName, lambdaClass);
        lambdaClass.u2superClass = constantPoolEditor.addClassConstant(ClassConstants.NAME_JAVA_LANG_OBJECT, null);

        ClassEditor classEditor = new ClassEditor(lambdaClass);

        String[] interfaces = lambdaExpression.interfaces;
        for (String interfaceName : interfaces) {
            classEditor.addInterface(constantPoolEditor.addClassConstant(interfaceName, null));
        }

        // Store the created lambda class in the LambdaExpression
        // data structure for later use.
        lambdaExpression.lambdaClass = lambdaClass;

        // Apply some optimizations to the lambda expression to
        // avoid creating accessor methods all the time.
        // fixAccessFlags(lambdaExpression);

        // In case the invoked method can not be accessed directly
        // by the lambda class, add a synthetic accessor method.
        if (lambdaExpression.needsAccessorMethod()) {
            addAccessorMethod(lambdaExpression.referencedClass, lambdaExpression);
        }

        if (lambdaExpression.isStateless()) {
            completeStatelessLambdaClass(lambdaClass, lambdaExpression);
        } else {
            completeCapturingLambdaClass(lambdaClass, lambdaExpression);
        }

        if (lambdaExpression.bridgeMethodDescriptors.length > 0) {
            addBridgeMethods(lambdaClass, lambdaExpression);
        }

        return lambdaClass;
    }

    private void fixAccessFlags(LambdaExpression lambdaExpression) {
        // If the invoked method is private, we can make it package-private
        // to be able to access it from the lambda class. Otherwise we would
        // need to add an additional accessor method.
        if (lambdaExpression.referencedInvokedMethod instanceof ProgramMethod) {
            ProgramMethod invokedProgramMethod = (ProgramMethod)lambdaExpression.referencedInvokedMethod;

            int currentAccessFlags = invokedProgramMethod.getAccessFlags();
            if ((currentAccessFlags & ClassConstants.ACC_PRIVATE) != 0) {
                invokedProgramMethod.u2accessFlags = AccessUtil.replaceAccessFlags(currentAccessFlags,
                    AccessUtil.accessFlags(AccessUtil.PACKAGE_VISIBLE));

                // In case of instance-capturing lambdas or method references to private
                // methods the reference kind is invokeSpecial. After fixing the
                // access flags we need to update the reference kind as well.
                if (lambdaExpression.invokedReferenceKind == ClassConstants.REF_invokeSpecial) {
                    lambdaExpression.invokedReferenceKind = ClassConstants.REF_invokeVirtual;
                }
            }
        }
    }

    private void addAccessorMethod(ProgramClass programClass, LambdaExpression lambdaExpression) {
        SimplifiedClassEditor classEditor = new SimplifiedClassEditor(programClass);

        String className = programClass.getName();

        // Create accessor method.
        String shortClassName = ClassUtil.externalShortClassName(ClassUtil.externalClassName(className));

        String accessorMethodName =
            String.format("accessor$%s$lambda%d", shortClassName, lambdaExpression.bootstrapMethodIndex);

        String accessorMethodDescriptor = lambdaExpression.invokedMethodDesc;
        int accessFlags = lambdaExpression.referencedInvokedMethod.getAccessFlags();

        if ((accessFlags & ClassConstants.ACC_STATIC) == 0) {
            accessorMethodDescriptor = prependParameterToMethodDescriptor(accessorMethodDescriptor,
                ClassUtil.internalTypeFromClassType(className));
        }

        CompactCodeAttributeComposer composer = classEditor.addMethod(
            ClassConstants.ACC_STATIC | ClassConstants.ACC_SYNTHETIC, accessorMethodName, accessorMethodDescriptor, 50);

        // Load the parameters first.
        InternalTypeEnumeration typeEnumeration = new InternalTypeEnumeration(accessorMethodDescriptor);

        completeInterfaceMethod(lambdaExpression, composer, 0, typeEnumeration, null);

        classEditor.finishEditing();

        // Update the lambda expression to point to the created
        // accessor method instead.
        lambdaExpression.invokedClassName = programClass.getName();
        lambdaExpression.invokedMethodName = accessorMethodName;
        lambdaExpression.invokedMethodDesc = accessorMethodDescriptor;
        lambdaExpression.invokedReferenceKind = ClassConstants.REF_invokeStatic;

        lambdaExpression.referencedInvokedClass = programClass;
        lambdaExpression.referencedInvokedMethod =
            programClass.findMethod(accessorMethodName, accessorMethodDescriptor);
    }

    private void completeStatelessLambdaClass(ProgramClass lambdaClass, LambdaExpression lambdaExpression) {
        String lambdaClassType = ClassUtil.internalTypeFromClassName(lambdaClass.getName());

        SimplifiedClassEditor classEditor = new SimplifiedClassEditor(lambdaClass);

        // Add singleton field
        classEditor.addField(ClassConstants.ACC_PUBLIC | ClassConstants.ACC_STATIC | ClassConstants.ACC_FINAL,
            LAMBDA_SINGLETON_FIELD_NAME, lambdaClassType);

        // Add the constructor.
        CompactCodeAttributeComposer composer = classEditor.addMethod(ClassConstants.ACC_PUBLIC,
            ClassConstants.METHOD_NAME_INIT, ClassConstants.METHOD_TYPE_INIT, 10);

        composer.aload_0().invokespecial(ClassConstants.NAME_JAVA_LANG_OBJECT, ClassConstants.METHOD_NAME_INIT,
            ClassConstants.METHOD_TYPE_INIT).return_();

        // Add static initializer.
        composer = classEditor.addMethod(ClassConstants.ACC_STATIC, ClassConstants.METHOD_NAME_CLINIT,
            ClassConstants.METHOD_TYPE_CLINIT, 30);

        composer.new_(lambdaClass.getName()).dup()
            .invokespecial(lambdaClass.getName(), ClassConstants.METHOD_NAME_INIT, ClassConstants.METHOD_TYPE_INIT)
            .putstatic(lambdaClass.getName(), LAMBDA_SINGLETON_FIELD_NAME, lambdaClassType).return_();

        // If the lambda expression is serializable, create a readResolve method
        // to return the singleton field.
        if (lambdaExpression.isSerializable()) {
            composer = classEditor.addMethod(ClassConstants.ACC_PRIVATE, ClassConstants.METHOD_NAME_READ_RESOLVE,
                ClassConstants.METHOD_TYPE_READ_RESOLVE, 10);

            composer.getstatic(lambdaClass.getName(), LAMBDA_SINGLETON_FIELD_NAME, lambdaClassType).areturn();
        }

        // Add the interface method.
        composer = classEditor.addMethod(ClassConstants.ACC_PUBLIC, lambdaExpression.interfaceMethod,
            lambdaExpression.interfaceMethodDescriptor, 50);

        if (lambdaExpression.invokedReferenceKind == ClassConstants.REF_newInvokeSpecial) {
            InternalTypeEnumeration typeEnumeration =
                new InternalTypeEnumeration(lambdaExpression.interfaceMethodDescriptor);

            InternalTypeEnumeration invokedTypeEnumeration =
                new InternalTypeEnumeration(lambdaExpression.invokedMethodDesc);

            composer.new_(lambdaExpression.invokedClassName).dup();

            // Convert the remaining parameters if they are present.
            completeInterfaceMethod(lambdaExpression, composer, 1, typeEnumeration, invokedTypeEnumeration);
        } else {
            InternalTypeEnumeration typeEnumeration =
                new InternalTypeEnumeration(lambdaExpression.interfaceMethodDescriptor);

            InternalTypeEnumeration invokedTypeEnumeration =
                new InternalTypeEnumeration(lambdaExpression.invokedMethodDesc);

            boolean isInvokeVirtualOrInterface =
                lambdaExpression.invokedReferenceKind == ClassConstants.REF_invokeVirtual
                    || lambdaExpression.invokedReferenceKind == ClassConstants.REF_invokeInterface;

            int paramIndex = 1;

            // If we invoke a method on an object, we need to cast it to the invoked type.
            if (isInvokeVirtualOrInterface) {
                String type = typeEnumeration.nextType();
                String invokedType = ClassUtil.internalTypeFromClassName(lambdaExpression.invokedClassName);

                composer.load(paramIndex, type);
                paramIndex += ClassUtil.internalTypeSize(type);

                convertToTargetType(type, invokedType, composer);
            }

            // Convert the remaining parameters if they are present.
            completeInterfaceMethod(lambdaExpression, composer, paramIndex, typeEnumeration, invokedTypeEnumeration);
        }

        classEditor.finishEditing();
    }

    private void completeCapturingLambdaClass(ProgramClass lambdaClass, LambdaExpression lambdaExpression) {
        SimplifiedClassEditor classEditor = new SimplifiedClassEditor(lambdaClass);

        // Create constructor.
        String ctorDescriptor = lambdaExpression.getConstructorDescriptor();
        CompactCodeAttributeComposer composer =
            classEditor.addMethod(ClassConstants.ACC_PUBLIC, ClassConstants.METHOD_NAME_INIT, ctorDescriptor, 50);

        composer.aload_0().invokespecial(ClassConstants.NAME_JAVA_LANG_OBJECT, ClassConstants.METHOD_NAME_INIT,
            ClassConstants.METHOD_TYPE_INIT);

        InternalTypeEnumeration typeEnumeration = new InternalTypeEnumeration(ctorDescriptor);

        int argIndex = 0;
        int variableIndex = 1;
        while (typeEnumeration.hasMoreTypes()) {
            String type = typeEnumeration.nextType();
            String fieldName = "arg$" + argIndex++;

            classEditor.addField(ClassConstants.ACC_PRIVATE | ClassConstants.ACC_FINAL, fieldName, type);

            composer.aload_0();
            composer.load(variableIndex, type);
            composer.putfield(lambdaClass.getName(), fieldName, type);

            variableIndex += ClassUtil.internalTypeSize(type);
        }

        composer.return_();

        // Create interface method.
        composer = classEditor.addMethod(ClassConstants.ACC_PUBLIC, lambdaExpression.interfaceMethod,
            lambdaExpression.interfaceMethodDescriptor, 50);

        // Load the instance fields first.
        typeEnumeration = new InternalTypeEnumeration(ctorDescriptor);

        InternalTypeEnumeration invokedTypeEnumeration =
            new InternalTypeEnumeration(lambdaExpression.invokedMethodDesc);

        boolean isInvokeVirtualOrInterface = lambdaExpression.invokedReferenceKind == ClassConstants.REF_invokeVirtual
            || lambdaExpression.invokedReferenceKind == ClassConstants.REF_invokeInterface;

        argIndex = 0;
        while (typeEnumeration.hasMoreTypes()) {
            String type = typeEnumeration.nextType();
            String invokedType = isInvokeVirtualOrInterface && argIndex == 0 ? null : invokedTypeEnumeration.nextType();

            String fieldName = "arg$" + argIndex++;

            composer.aload_0().getfield(lambdaClass.getName(), fieldName, type);

            if (invokedType != null) {
                convertToTargetType(type, invokedType, composer);
            }
        }

        // And then the method parameters.
        typeEnumeration = new InternalTypeEnumeration(lambdaExpression.interfaceMethodDescriptor);

        completeInterfaceMethod(lambdaExpression, composer, 1, typeEnumeration, invokedTypeEnumeration);

        classEditor.finishEditing();
    }

    private void completeInterfaceMethod(LambdaExpression lambdaExpression, CompactCodeAttributeComposer composer,
        int parameterIndex, InternalTypeEnumeration typeEnumeration, InternalTypeEnumeration invokedTypeEnumeration) {
        while (typeEnumeration.hasMoreTypes()) {
            String type = typeEnumeration.nextType();
            String invokedType = invokedTypeEnumeration != null ? invokedTypeEnumeration.nextType() : null;

            composer.load(parameterIndex, type);
            parameterIndex += ClassUtil.internalTypeSize(type);

            if (invokedType != null) {
                convertToTargetType(type, invokedType, composer);
            }
        }

        switch (lambdaExpression.invokedReferenceKind) {
            case ClassConstants.REF_invokeStatic:
                if (lambdaExpression.invokesStaticInterfaceMethod()) {
                    composer.invokestaticinterface(lambdaExpression.invokedClassName,
                        lambdaExpression.invokedMethodName, lambdaExpression.invokedMethodDesc,
                        lambdaExpression.referencedInvokedClass, lambdaExpression.referencedInvokedMethod);
                } else {
                    composer.invokestatic(lambdaExpression.invokedClassName, lambdaExpression.invokedMethodName,
                        lambdaExpression.invokedMethodDesc, lambdaExpression.referencedInvokedClass,
                        lambdaExpression.referencedInvokedMethod);
                }
                break;

            case ClassConstants.REF_invokeVirtual:
                composer.invokevirtual(lambdaExpression.invokedClassName, lambdaExpression.invokedMethodName,
                    lambdaExpression.invokedMethodDesc, lambdaExpression.referencedInvokedClass,
                    lambdaExpression.referencedInvokedMethod);
                break;

            case ClassConstants.REF_invokeInterface:
                composer.invokeinterface(lambdaExpression.invokedClassName, lambdaExpression.invokedMethodName,
                    lambdaExpression.invokedMethodDesc, lambdaExpression.referencedInvokedClass,
                    lambdaExpression.referencedInvokedMethod);
                break;

            case ClassConstants.REF_newInvokeSpecial:
            case ClassConstants.REF_invokeSpecial:
                composer.invokespecial(lambdaExpression.invokedClassName, lambdaExpression.invokedMethodName,
                    lambdaExpression.invokedMethodDesc, lambdaExpression.referencedInvokedClass,
                    lambdaExpression.referencedInvokedMethod);
                break;
        }

        String methodReturnType = typeEnumeration.returnType();

        if (invokedTypeEnumeration != null) {
            convertToTargetType(invokedTypeEnumeration.returnType(), methodReturnType, composer);
        }

        composer.return_(methodReturnType);
    }

    private void addBridgeMethods(ProgramClass lambdaClass, LambdaExpression lambdaExpression) {
        SimplifiedClassEditor classEditor = new SimplifiedClassEditor(lambdaClass);

        String methodName = lambdaExpression.interfaceMethod;
        for (String bridgeMethodDescriptor : lambdaExpression.bridgeMethodDescriptors) {
            Method method = lambdaClass.findMethod(methodName, bridgeMethodDescriptor);
            if (method != null) {
                continue;
            }

            CompactCodeAttributeComposer composer = classEditor.addMethod(
                ClassConstants.ACC_PUBLIC | ClassConstants.ACC_SYNTHETIC | ClassConstants.ACC_BRIDGE, methodName,
                bridgeMethodDescriptor, 50);

            composer.aload_0();

            InternalTypeEnumeration interfaceTypeEnumeration =
                new InternalTypeEnumeration(lambdaExpression.interfaceMethodDescriptor);

            InternalTypeEnumeration bridgeTypeEnumeration = new InternalTypeEnumeration(bridgeMethodDescriptor);
            int variableIndex = 1;
            while (bridgeTypeEnumeration.hasMoreTypes()) {
                String type = bridgeTypeEnumeration.nextType();
                String interfaceType = interfaceTypeEnumeration.nextType();

                composer.load(variableIndex, type);
                variableIndex += ClassUtil.internalTypeSize(type);

                convertToTargetType(type, interfaceType, composer);
            }

            composer.invokevirtual(lambdaClass.getName(), lambdaExpression.interfaceMethod,
                lambdaExpression.interfaceMethodDescriptor);

            String methodReturnType = bridgeTypeEnumeration.returnType();

            convertToTargetType(interfaceTypeEnumeration.returnType(), methodReturnType, composer);

            composer.return_(methodReturnType);
        }

        classEditor.finishEditing();
    }

    private static String prependParameterToMethodDescriptor(String methodDescriptor, String type) {
        StringBuilder methodDescBuilder = new StringBuilder();

        methodDescBuilder.append('(');
        methodDescBuilder.append(type);

        InternalTypeEnumeration typeEnumeration = new InternalTypeEnumeration(methodDescriptor);

        while (typeEnumeration.hasMoreTypes()) {
            methodDescBuilder.append(typeEnumeration.nextType());
        }

        methodDescBuilder.append(')');
        methodDescBuilder.append(typeEnumeration.returnType());
        return methodDescBuilder.toString();
    }

    /**
     * Adds the required instructions to the provided CodeAttributeComposer to convert the current value on the stack to
     * the given targetType.
     */
    private static void convertToTargetType(String sourceType, String targetType,
        CompactCodeAttributeComposer composer) {
        if (ClassUtil.isInternalPrimitiveType(sourceType) && !ClassUtil.isInternalPrimitiveType(targetType)) {
            // Perform auto-boxing.
            switch (sourceType.charAt(0)) {
                case ClassConstants.TYPE_INT:
                    composer.invokestatic(ClassConstants.NAME_JAVA_LANG_INTEGER, "valueOf", "(I)Ljava/lang/Integer;");
                    break;

                case ClassConstants.TYPE_BYTE:
                    composer.invokestatic(ClassConstants.NAME_JAVA_LANG_BYTE, "valueOf", "(B)Ljava/lang/Byte;");
                    break;

                case ClassConstants.TYPE_CHAR:
                    composer.invokestatic(ClassConstants.NAME_JAVA_LANG_CHARACTER, "valueOf",
                        "(C)Ljava/lang/Character;");
                    break;

                case ClassConstants.TYPE_SHORT:
                    composer.invokestatic(ClassConstants.NAME_JAVA_LANG_SHORT, "valueOf", "(S)Ljava/lang/Short;");
                    break;

                case ClassConstants.TYPE_BOOLEAN:
                    composer.invokestatic(ClassConstants.NAME_JAVA_LANG_BOOLEAN, "valueOf", "(Z)Ljava/lang/Boolean;");
                    break;

                case ClassConstants.TYPE_LONG:
                    composer.invokestatic(ClassConstants.NAME_JAVA_LANG_LONG, "valueOf", "(J)Ljava/lang/Long;");
                    break;

                case ClassConstants.TYPE_FLOAT:
                    composer.invokestatic(ClassConstants.NAME_JAVA_LANG_FLOAT, "valueOf", "(F)Ljava/lang/Float;");
                    break;

                case ClassConstants.TYPE_DOUBLE:
                    composer.invokestatic(ClassConstants.NAME_JAVA_LANG_DOUBLE, "valueOf", "(D)Ljava/lang/Double;");
                    break;
            }
        } else if (!ClassUtil.isInternalPrimitiveType(sourceType) && ClassUtil.isInternalPrimitiveType(targetType)) {
            boolean castRequired = sourceType.equals(ClassConstants.TYPE_JAVA_LANG_OBJECT);

            // Perform auto-unboxing.
            switch (targetType.charAt(0)) {
                case ClassConstants.TYPE_INT:
                    if (castRequired) {
                        composer.checkcast("java/lang/Number");
                    }
                    composer.invokevirtual("java/lang/Number", "intValue", "()I");
                    break;

                case ClassConstants.TYPE_BYTE:
                    if (castRequired) {
                        composer.checkcast(ClassConstants.NAME_JAVA_LANG_BYTE);
                    }
                    composer.invokevirtual(ClassConstants.NAME_JAVA_LANG_BYTE, "byteValue", "()B");
                    break;

                case ClassConstants.TYPE_CHAR:
                    if (castRequired) {
                        composer.checkcast(ClassConstants.NAME_JAVA_LANG_CHARACTER);
                    }
                    composer.invokevirtual(ClassConstants.NAME_JAVA_LANG_CHARACTER, "charValue", "()C");
                    break;

                case ClassConstants.TYPE_SHORT:
                    if (castRequired) {
                        composer.checkcast(ClassConstants.NAME_JAVA_LANG_SHORT);
                    }
                    composer.invokevirtual(ClassConstants.NAME_JAVA_LANG_SHORT, "shortValue", "()S");
                    break;

                case ClassConstants.TYPE_BOOLEAN:
                    if (castRequired) {
                        composer.checkcast(ClassConstants.NAME_JAVA_LANG_BOOLEAN);
                    }
                    composer.invokevirtual(ClassConstants.NAME_JAVA_LANG_BOOLEAN, "booleanValue", "()Z");
                    break;

                case ClassConstants.TYPE_LONG:
                    if (castRequired) {
                        composer.checkcast("java/lang/Number");
                    }
                    composer.invokevirtual("java/lang/Number", "longValue", "()J");
                    break;

                case ClassConstants.TYPE_FLOAT:
                    if (castRequired) {
                        composer.checkcast("java/lang/Number");
                    }
                    composer.invokevirtual("java/lang/Number", "floatValue", "()F");
                    break;

                case ClassConstants.TYPE_DOUBLE:
                    if (castRequired) {
                        composer.checkcast("java/lang/Number");
                    }
                    composer.invokevirtual("java/lang/Number", "doubleValue", "()D");
                    break;
            }
        } else if (ClassUtil.isInternalClassType(sourceType)
            && (ClassUtil.isInternalClassType(targetType) || ClassUtil.isInternalArrayType(targetType))
            && !sourceType.equals(targetType) &&
            // No need to cast to java/lang/Object.
            !ClassConstants.TYPE_JAVA_LANG_OBJECT.equals(targetType)) {
            // Cast to target type.
            composer.checkcast(ClassUtil.internalClassTypeFromType(targetType));
        }
    }
}
